﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Documents;
using System.Collections.ObjectModel;
using System.Windows;
using SmartPaginator.TextFormatting;
using System.Windows.Media.TextFormatting;
using System.Windows.Media;
using System.Windows.Controls;
using SmartPaginator.Extensions;

namespace SmartPaginator.Elements
{
    public class ParagraphElement: Canvas, IBlockElement
    {
        #region Constructor

        protected ParagraphElement()
        {
            this.Height = 0;
            this.HorizontalAlignment = HorizontalAlignment.Left;
        }

        public ParagraphElement(Paragraph source, IElementFactory factory)
        {
            Source = source;
            this.Background = source.Background;
            this.Margin = source.CalculateVisualMargin();
            this.KeepTogether = source.KeepTogether;
            this.BreakPageBefore = source.BreakPageBefore;
            this.Height = 0;
            this.HorizontalAlignment = HorizontalAlignment.Left;
            TextParagraphProperties = new SmartTextParagraphProperties(source);

            // Create the figure elements
            // Note: the number of lines will vary depending on our width, so they are calculated on MeasureOverride. However,
            // the number of figures is constant and we need the factory to create them, so they are created here.
            LeftSideFigures = new List<FigureElement>();
            RightSideFigures = new List<FigureElement>();
            Lines = new List<LineElement>();

            foreach (var inline in source.Inlines)
            {
                var figure = inline as Figure;
                if (figure != null)
                {
                    var element = new FigureElement(figure, factory);

                    // Treat all the cases below as relative to the paragraph
                    switch (element.Figure.HorizontalAnchor)
                    {
                        case FigureHorizontalAnchor.ColumnRight:
                        case FigureHorizontalAnchor.ContentRight:
                        case FigureHorizontalAnchor.PageRight:
                            RightSideFigures.Add(element);
                            break;

                        default:
                            LeftSideFigures.Add(element);
                            break;
                    }
                    Children.Add(element);
                }
            }
        }

        #endregion

        #region Properties

        public Paragraph Source { get; private set; }

        public LineElement CurrentLine { get; private set; }

        public SmartTextParagraphProperties TextParagraphProperties { get; private set; }

        public IBlockContainer ParentContainer
        {
            get { return this.Parent as IBlockContainer; }
        }

        public bool KeepTogether { get; private set; }

        public bool BreakPageBefore { get; set; }

        public List<FigureElement> LeftSideFigures { get; private set; }

        public List<FigureElement> RightSideFigures { get; private set; }

        public List<LineElement> Lines { get; private set; }

        #endregion

        #region GetEmptyCopy Method

        public IBlockElement GetEmptyCopy()
        {
            var result = new ParagraphElement();
            result.Background = this.Background;
            result.Width = this.Width;
            result.MaxWidth = this.MaxWidth;
            return result;
        }

        #endregion

        #region Figure Methods

        private void PositionLeftSideFigures()
        {
            double top = 0;

            foreach (var element in LeftSideFigures)
            {
                Canvas.SetLeft(element, 0);
                Canvas.SetTop(element, top);
                top = top + element.Height + element.Margin.Bottom + element.Margin.Top;

                if (double.IsNaN(this.Height) || this.Height < top)
                    this.Height = top;
            }
        }

        private void PositionRightSideFigures()
        {
            double top = 0;
            double right = 0;
            if (double.IsNaN(this.MaxWidth) || double.IsInfinity(this.MaxWidth))
            {
                // We need to calculate the right edge ourselves
            }
            else
            {
                right = this.MaxWidth;
            }

            var positionedFigures = new List<FigureElement>();
            foreach (var element in RightSideFigures)
            {
                if (positionedFigures.Count > 0)
                {
                    var lastFigure = positionedFigures[positionedFigures.Count - 1];
                    top = Canvas.GetTop(lastFigure) + lastFigure.Height + lastFigure.Margin.Bottom;
                }
                positionedFigures.Add(element);

                var left = right - (element.Width + element.Margin.Left + element.Margin.Right);
                var elementSize = new Size(element.Width + element.Margin.Left + element.Margin.Right,
                                           element.Height + element.Margin.Top + element.Margin.Bottom);

                foreach (var leftFigure in LeftSideFigures)
                {
                    var elementLocation = new Point(left, top);
                    var elementBounds = new Rect(elementLocation, elementSize);

                    var leftFigureBounds = leftFigure.GetBounds();

                    if (CheckIfElementsIntersect(elementBounds, leftFigureBounds))
                        top = leftFigureBounds.Bottom;
                }

                foreach (var line in Lines)
                {
                    var elementLocation = new Point(left, top);
                    var elementBounds = new Rect(elementLocation, elementSize);

                    var lineLocation = new Point(Canvas.GetLeft(line), Canvas.GetTop(line));
                    var lineBounds = new Rect(lineLocation, new Size(line.Width, line.Height));

                    if (CheckIfElementsIntersect(elementBounds, lineBounds))
                        top = lineBounds.Bottom;
                }

                Canvas.SetTop(element, top);
                Canvas.SetLeft(element, left);
                top = top + element.Height + element.Margin.Top + element.Margin.Bottom;

                if (double.IsNaN(this.Height) || this.Height < top)
                    this.Height = top;
            }
        }
        
        private void MeasureAllFigures()
        {
            foreach (var figure in LeftSideFigures)
                MeasureFigure(figure);

            foreach (var figure in RightSideFigures)
                MeasureFigure(figure);
        }


        private void MeasureFigure(FigureElement element)
        {
            // Set the element width
            switch (element.Figure.Width.FigureUnitType)
            {
                case FigureUnitType.Pixel:
                    element.Width = element.Figure.Width.Value;
                    break;

                case FigureUnitType.Column:
                case FigureUnitType.Content:
                case FigureUnitType.Page:
                    // Treat as relative to the paragraph instead. Also, cap at at the size of the paragraph
                    element.Width = Math.Min(1, element.Figure.Width.Value) * this.MaxWidth;
                    break;

                case FigureUnitType.Auto:
                    element.Width = double.NaN;
                    break;
            }

            // Set the element height
            switch (element.Figure.Height.FigureUnitType)
            {
                case FigureUnitType.Pixel:
                    element.Height = element.Figure.Height.Value;
                    break;

                case FigureUnitType.Column:
                case FigureUnitType.Content:
                case FigureUnitType.Page:
                case FigureUnitType.Auto:
                    // Treat all relative heights as auto
                    element.Height = double.NaN;
                    break;
            }

            // Measure the figure
            var size = new Size(this.MaxWidth, double.PositiveInfinity);
            element.Measure(size);
            element.Height = element.DesiredSize.Height - element.Margin.Top - element.Margin.Bottom;
            element.Width = element.DesiredSize.Width - element.Margin.Left - element.Margin.Right;
        }

        private bool CheckIfElementsIntersect(Rect rect1, Rect rect2)
        {
            if (rect1.IsEmpty || rect2.IsEmpty)
            {
                return false;
            }
            return ((((rect2.Left < rect1.Right) && (rect2.Right > rect1.Left)) && (rect2.Top < rect1.Bottom)) && (rect2.Bottom > rect1.Top));
        }

        #endregion

        #region Lines Methods

        private void PositionLines()
        {
            // Delete all the previously created lines
            foreach (var line in Lines)
                if (Children.Contains(line))
                    Children.Remove(line);
            Lines.Clear();


            // Create a TextSource and a TextFormatter objects
            var textSource = new SmartTextSource(Source.Inlines);
            var formatter = TextFormatter.Create(TextFormattingMode.Ideal);
            int index = 0;
            
            // Start at the top
            Rect lineBounds = GetLineBoundsStartingAt(0, 0);

            // Format each line of text from the text source and draw it.
            while (index < textSource.Length)
            {
                double remainingWidth = GetRemainingWidth(lineBounds.Width);
                if (remainingWidth <= 0)
                {
                    CurrentLine = new LineElement();
                    lineBounds = GetLineBoundsStartingAt(lineBounds.Bottom, 0);
                    continue;
                }

                // Create a textline from the text store using the TextFormatter object.
                using (var line = formatter.FormatLine(textSource,
                                                        index,
                                                        remainingWidth,
                                                        TextParagraphProperties,
                                                        null))
                {
                    if (line.HasOverflowed || line.IsTruncated)
                    {
                        // Can't fit the text
                        bool addLineBreak = false;
                        if (remainingWidth != lineBounds.Width)
                        {
                            // Means we already wrote something to this line, so we can safely add a line break and try again
                            addLineBreak = true;
                        }
                        if (lineBounds.Width != this.MaxWidth)
                        {
                            // We haven't written anything. If the line's width is not the same as the paragraph's,
                            // it means we have figures and we can safely add line breaks until we reach the end of the figures
                            addLineBreak = true;
                        }

                        if (addLineBreak)
                        {
                            if (CurrentLine.Children.Count == 0)
                            {
                                AddInline(new InlineElement(0, line.Height), 0);
                                Canvas.SetTop(CurrentLine, lineBounds.Top);
                                Canvas.SetLeft(CurrentLine, lineBounds.Left);
                            }

                            CurrentLine = new LineElement();
                            lineBounds = GetLineBoundsStartingAt(lineBounds.Bottom, 0);
                            continue;
                        }
                        else
                        {
                            // Do nothing. We'll write the text anyway, even though it is truncated.
                            // this is because we have no way to make the paragraph widder, so we have no other choice than to truncate.

                            // Note that if we chose to add a line break and try again in this case, we would reach an infinite loop!
                        }
                    }

                    // Now that we know the actual height of the line, let's update the bounds and,
                    // if we did it, recheck to see if the content still fits
                    if (line.Height > lineBounds.Height)
                    {
                        lineBounds.Height = line.Height;

                        foreach (var figure in LeftSideFigures)
                        {
                            var figureBounds = figure.GetBounds();
                            if (CheckIfElementsIntersect(lineBounds, figureBounds))
                            {
                                CurrentLine = new LineElement();
                                lineBounds = GetLineBoundsStartingAt(figureBounds.Bottom, 0);
                                continue;
                            }
                        }
                        foreach (var figure in RightSideFigures)
                        {
                            var figureBounds = figure.GetBounds();

                            if (CheckIfElementsIntersect(lineBounds, figureBounds))
                            {
                                CurrentLine = new LineElement();
                                lineBounds = GetLineBoundsStartingAt(figureBounds.Bottom, 0);
                                continue;
                            }
                        }
                    }

                    // If we got here, the line fits
                    double trailingSpace = line.WidthIncludingTrailingWhitespace - line.Width;
                    var element = new InlineElement(line.Width, line.Height);
                    AddInline(element, trailingSpace);
                    Canvas.SetTop(CurrentLine, lineBounds.Top);
                    Canvas.SetLeft(CurrentLine, lineBounds.Left);

                    using (var context = element.RenderOpen())
                    {
                        // Draw the formatted text into the visual.
                        line.Draw(context, new Point(), InvertAxes.None);
                    }


                    if ((line.NewlineLength > 0) || (line.HasOverflowed || line.IsTruncated))
                    {
                        // Line ends with a line-break, or the text was too long and is being clipped/truncated
                        CurrentLine = new LineElement();
                        lineBounds = GetLineBoundsStartingAt(lineBounds.Bottom, 0);
                    }

                    index = index + line.Length;
                }
            }
        }


        private Rect GetLineBoundsStartingAt(double top, double minWidth)
        {
            if (Lines.Count > 0)
            {
                var lastLine = Lines[Lines.Count - 1];
                top = Canvas.GetTop(lastLine) + lastLine.Height;
            }

            double lineHeight = TextElementExtensions.GetLineHeight(this.Source);
            double left = 0;
            double width = this.MaxWidth;
            if (double.IsNaN(width) || double.IsInfinity(width))
                width = MAX_PARAGRAPH_WIDTH;
            if (width == 0)
                width = lineHeight;

            var lineLocation = new Point(left, top);
            var lineBounds = new Rect(lineLocation, new Size(width, lineHeight));

            foreach (var figure in LeftSideFigures)
            {
                var figureBounds = figure.GetBounds();

                if (CheckIfElementsIntersect(lineBounds, figureBounds))
                {
                    left = figureBounds.Right;
                    width = (lineBounds.Right - figureBounds.Right);
                    if (width <= minWidth)
                        return GetLineBoundsStartingAt(figureBounds.Bottom, minWidth);
                }

                // Update location
                lineLocation = new Point(left, top);
                lineBounds = new Rect(lineLocation, new Size(width, lineHeight));
            }

            if (!double.IsInfinity(width))
            {
                foreach (var figure in RightSideFigures)
                {
                    var figureBounds = figure.GetBounds();

                    if (CheckIfElementsIntersect(lineBounds, figureBounds))
                    {
                        width = (figureBounds.Left - lineBounds.Left);
                        if (width <= minWidth)
                            return GetLineBoundsStartingAt(figureBounds.Bottom, minWidth);
                    }

                    // Update location
                    lineLocation = new Point(left, top);
                    lineBounds = new Rect(lineLocation, new Size(width, lineHeight));
                }
            }

            return lineBounds;
        }


        private double GetRemainingWidth(double width)
        {
            if (CurrentLine == null)
                return width;

            return CurrentLine.GetRemainingWidth(width);
        }


        public void AddInline(InlineElement element, double trailingSpace)
        {
            if (CurrentLine == null)
                CurrentLine = new LineElement();

            CurrentLine.AddInline(element, trailingSpace);
            if (!Lines.Contains(CurrentLine))
            {
                Lines.Add(CurrentLine);
                Children.Add(CurrentLine);
            }
        }

        #endregion

        #region Harvest Method

        public HarvestResult Harvest(HarvestingContext context, double remainingHeight, IBlockElement previousElement)
        {
            bool isPartiallyHarvested = context.IsPartiallyHarvested(this);

            // If the paragraph requires a page break, let's make sure we are the first in the document.
            if ((this.BreakPageBefore) && (PageColumnElement.IsDirectChild(this)) && !isPartiallyHarvested)
            {
                if (previousElement != null)
                {
                    return HarvestResult.BreakPage;
                }
            }

            // First, let's adjust the margins
            Thickness adjustedMargin;
            double bottomMargin;
            if (!isPartiallyHarvested)
            {
                adjustedMargin = BlockHelper.GetAdjustedMargin(previousElement, this, Source.Margin);
            }
            else
            {
                adjustedMargin = BlockHelper.GetAdjustedMargin(null, this, Source.Margin);
                adjustedMargin.Top = 0;
            }
            remainingHeight -= adjustedMargin.Top;

            // We'll re-add the bottom margin at the end, if we are can harvest the entire the paragraph
            bottomMargin = adjustedMargin.Bottom;
            adjustedMargin.Bottom = 0;

            // Now, let's see if we can fit the minimum size
            if (!isPartiallyHarvested && (this.CalculateMinHeight() > remainingHeight))
            {
                // We can't fit the paragraph, so break the column
                return HarvestResult.BreakColumn;
            }

            // Now, let's find out where do we start this harvesting pass
            double startingPoint = GetStartingHarvestPoint(context);
            double harvestStartingPoint = startingPoint;

            // Find out what's the next breaking point
            double nextBreakingPoint = GetNextBreakingPoint(startingPoint);

            var harvestedElement = this.GetEmptyCopy() as ParagraphElement;
            harvestedElement.Margin = adjustedMargin;

            // Keep track of the harvested height
            double height = 0;

            while ((startingPoint == harvestStartingPoint) ||  // Make sure we harvest at least once, even if it doesn't fit. Content will be clipped though
                   (remainingHeight - (nextBreakingPoint - startingPoint) > 0))
            { 
                // Harvest everything between starting point and breaking point
                foreach (var figure in LeftSideFigures)
                {
                    if (context.IsFullyHarvested(figure))
                        continue;

                    double top = Canvas.GetTop(figure);
                    if ((top >= startingPoint) && (top < nextBreakingPoint))
                    {
                        var copy = figure.GetCopy();
                        Canvas.SetTop(copy, top - harvestStartingPoint);
                        harvestedElement.Children.Add(copy);
                        context.MarkAsFullyHarvested(figure);
                    }
                }

                foreach (var figure in RightSideFigures)
                {
                    if (context.IsFullyHarvested(figure))
                        continue;

                    double top = Canvas.GetTop(figure);
                    if ((top >= startingPoint) && (top < nextBreakingPoint))
                    {
                        var copy = figure.GetCopy();
                        Canvas.SetTop(copy, top - harvestStartingPoint);
                        harvestedElement.Children.Add(copy);
                        context.MarkAsFullyHarvested(figure);
                    }
                }

                foreach (var line in Lines)
                {
                    if (context.IsFullyHarvested(line))
                        continue;

                    double top = Canvas.GetTop(line);
                    if ((top >= startingPoint) && (top < nextBreakingPoint))
                    {
                        var copy = line.GetCopy();
                        Canvas.SetTop(copy, top - harvestStartingPoint);
                        harvestedElement.Children.Add(copy);
                        context.MarkAsFullyHarvested(line);
                    }
                }

                height += (nextBreakingPoint - startingPoint);
                remainingHeight -= (nextBreakingPoint - startingPoint);
                startingPoint = nextBreakingPoint;
                nextBreakingPoint = GetNextBreakingPoint(startingPoint);

                if (startingPoint == nextBreakingPoint)
                {
                    context.MarkAsFullyHarvested(this);
                    break;
                }
            }

            harvestedElement.Height = height;

            if (context.IsFullyHarvested(this))
            {
                adjustedMargin.Bottom = bottomMargin;
                harvestedElement.Margin = adjustedMargin;
                remainingHeight -= bottomMargin;
            }

            return new HarvestResult(harvestedElement, remainingHeight);
        }

        private double GetStartingHarvestPoint(HarvestingContext context)
        {
            double result = 0;
            foreach(var figure in LeftSideFigures)
            {
                if (context.IsFullyHarvested(figure))
                {
                    var bottom = Canvas.GetTop(figure) + figure.Height;
                    if (bottom > result)
                        result = bottom;
                }
            }

            foreach (var figure in RightSideFigures)
            {
                if (context.IsFullyHarvested(figure))
                {
                    var bottom = Canvas.GetTop(figure) + figure.Height;
                    if (bottom > result)
                        result = bottom;
                }
            }

            foreach (var line in Lines)
            {
                if (context.IsFullyHarvested(line))
                {
                    var bottom = Canvas.GetTop(line) + line.Height;
                    if (bottom > result)
                        result = bottom;
                }
            }

            return result;
        }

        #endregion

        #region Margin Calculation

        public virtual double CalculateBottomMargin()
        {
            if (double.IsNaN(Source.Margin.Bottom))
                return Source.GetLineHeight();

            return this.Margin.Bottom;
        }

        public virtual double CalculateTopMargin()
        {
            if (double.IsNaN(Source.Margin.Top))
                return Source.GetLineHeight();

            return this.Margin.Top;
        }

        #endregion

        #region Measure and Arrange Overrides

        protected override Size MeasureOverride(Size constraint)
        {
            if (Source != null)
            {
                this.MaxWidth = constraint.Width;
                MeasureAllFigures();
                PositionLeftSideFigures();
                if (!double.IsNaN(this.MaxWidth) && !double.IsInfinity(this.MaxWidth))
                {
                    PositionRightSideFigures();
                    PositionLines();
                }
                else
                {
                    PositionLines();
                    PositionRightSideFigures();
                }
                UpdateSize();
            }
            
            var result = base.MeasureOverride(constraint);
            if (!double.IsNaN(this.Width))
                result.Width = this.Width;
            if (!double.IsNaN(this.Height))
                result.Height = this.Height;
            return result;
        }

        private void UpdateSize()
        {
            this.Height = 0;
            this.Width = 0;
            foreach (var figure in LeftSideFigures)
            {
                var bottom = Canvas.GetTop(figure) + figure.Height;
                if (bottom > this.Height)
                    this.Height = bottom;
                var right = Canvas.GetLeft(figure) + figure.Width;
                if (right > this.Width)
                    this.Width = right;
            }
            foreach (var figure in RightSideFigures)
            {
                var bottom = Canvas.GetTop(figure) + figure.Height;
                if (bottom > this.Height)
                    this.Height = bottom;
                var right = Canvas.GetLeft(figure) + figure.Width;
                if (right > this.Width)
                    this.Width = right;
            }
            foreach (var line in Lines)
            {
                var bottom = Canvas.GetTop(line) + line.Height;
                if (bottom > this.Height)
                    this.Height = bottom;
                var right = Canvas.GetLeft(line);
                if (!double.IsNaN(line.Width) && !double.IsInfinity(line.Width))
                    right += line.Width;
                else
                    right += line.CurrentWidth;
                if (right > this.Width)
                    this.Width = right;
            }
        }

        protected override Size ArrangeOverride(Size arrangeBounds)
        {
            var result = base.ArrangeOverride(arrangeBounds);
            return result;
        }

        #endregion

        #region CalculateHeight Methods

        public double CalculateHeight()
        {
            if (double.IsNaN(this.Height))
                return 0;

            return this.Height;
        }

        public virtual double CalculateMinHeight()
        {
            if (Children.Count > 0)
            {
                if (this.KeepTogether)
                {
                    return CalculateHeight();
                }
                else
                {
                    return GetNextBreakingPoint(0);
                }
            }

            return 0;
        }

        #endregion

        #region GetNextBreakingPoint Method

        protected double GetNextBreakingPoint(double currentBreakingPoint)
        {
            double result = currentBreakingPoint;
            bool keepChecking = true;
            while (keepChecking)
            {
                keepChecking = false;
                foreach (var figure in LeftSideFigures)
                {
                    double top = Canvas.GetTop(figure);
                    double bottom = top + figure.Height;
                    if (((top <= currentBreakingPoint) || (top < result)) && (bottom > result))
                    {
                        result = bottom;
                        keepChecking = true;
                        break;
                    }
                }

                if (keepChecking)
                    continue;

                foreach (var figure in RightSideFigures)
                {
                    double top = Canvas.GetTop(figure);
                    double bottom = top + figure.Height;
                    if (((top <= currentBreakingPoint) || (top < result)) && (bottom > result))
                    {
                        result = bottom;
                        keepChecking = true;
                        break;
                    }
                }

                if (keepChecking)
                    continue;

                foreach (var line in Lines)
                {
                    double top = Canvas.GetTop(line);
                    double bottom = top + line.Height;
                    if (((top <= currentBreakingPoint) || (top < result)) && (bottom > result))
                    {
                        result = bottom;
                        keepChecking = true;
                        break;
                    }
                }
            }

            return result;
        }

        #endregion

        #region Constants

        private const double MAX_PARAGRAPH_WIDTH = 3579139.4066666667;

        #endregion

    }
}
